HEAD:docs/articles/Gallery/Gallery.html
vignettes/Gallery/Gallery.Rmd
Gallery.RmdAs a kid, I loved making spirographs. I still do. Making them feels more like discovery than creativity, like finding hidden wings in the Mathematical Museum of Art. I have not yet found the point where spirographs no longer surprise me.
The surprising variety of forms generated by spirographs are manifestations of just one equation, the circular path troichoid. The shape of the spirograph depends on the radius of a fixed circle, radius of a cycling circle, and the distance of the pen from the center of the the cycling circle.
\[\begin{align} x (\theta) &= (R - r)\cos\theta + d\cos\left({R - r \over r}\theta\right)\\ y (\theta) &= (R - r)\sin\theta - d\sin\left({R - r \over r}\theta\right) \end{align} \]
Where
R is the radius of the fixed circle
r is the radius of the cycling circle
d is the distance of the pen from the center of the cycling circle
θ is the number of radians the cycling circle travels around the fixed circle
x(θ) is the position of x after the cycling circle travels θ radians
y(θ) is the position of y after the cycling circle travels θ radians
cycling_radius <- 1 fixed_radius <- 3 pen_radius <- 2 d_circle <- tibble( x0 = c(0, fixed_radius - cycling_radius), y0 = c(0, 0), radius = c(fixed_radius, cycling_radius), r_y = c(0, 0), r_x = c(-fixed_radius / 2, fixed_radius - 1.5 * cycling_radius), r_lab = c("Fixed\nRadius", "Cycling\nRadius"), color = c("black", "royalblue") ) d_segment <- tibble( x = c(0, fixed_radius - cycling_radius, fixed_radius - cycling_radius), y = c(0, 0, 0), xend = c(-fixed_radius + 0.04, fixed_radius - cycling_radius * 2 + 0.04, fixed_radius - cycling_radius + pen_radius - 0.04), yend = c(0, 0, 0), color = c("black", "royalblue", "firebrick") ) ggplot(data = d_circle) + theme_void() + ggforce::geom_circle( aes( x0 = x0, y0 = y0, r = radius, color = color), n = 1000) + coord_equal() + geom_text( aes( x = r_x, y = r_y, label = r_lab, color = color), vjust = 0.5, nudge_y = 0.015, angle = 0) + annotate( x = fixed_radius - cycling_radius + pen_radius / 2, y = 0.015, geom = "label", color = "firebrick", label = "Pen\nDistance", label.size = 0, label.padding = unit(3, "pt")) + geom_segment( data = d_segment, aes(x = x, y = y, xend = xend, yend = yend, color = color), geom = "segment", linejoin = "mitre", arrow = arrow( length = unit(0.025, "npc"), type = "closed", angle = 15)) + annotate( x = fixed_radius - cycling_radius, y = 0, geom = "point", color = "royalblue") + scale_color_identity() + theme(legend.position = "none")
Three parameters of spirograph shapes

In this spirograph,
the fixed radius R is 3,
the cycling radius r is 1,
and the pen radius d is 2.
Although I still like making spirographs by hand, I wanted to extend what could be done with the traditional spirograph. I wrote the spiro package in R to make images that would be impossible to create on paper.
I cannot usually predict what will happen when I play with the three primary numbers of the equation. However, once a certain combination strikes me as interesting, I play with cutting it into different color segments to see if something of further interest happens. Sometimes I merge many spirographs and spin them to see if the emerging patterns are pleasing.
Here I demonstrate what can be done with spiro package. I would love to see what you can do with it.
spiro( fixed_radius = 1231, cycling_radius = 529, pen_radius = 1233, colors = viridis::viridis(67), color_groups = 67, color_cycles = 59, windings = 96, points_per_polygon = 50, file = "viridis_weave.svg" ) %>% add_background(color = "gray8")
tibble::tibble( points_per_polygon = 1000, fixed_radius = 17, cycling_radius = 3:8, colors = c( "dodgerblue4", "white", "dodgerblue3", "white", "dodgerblue2", "white")) %>% pmap_chr(spiro) %>% image_merge( output = "i_saw_your_movie.svg")
n <- 25 cc <- ochre_palettes$emu_woman_paired[c(6, 11, 2, 7, 9)] %>% rep(5) tibble::tibble( fixed_radius = n + 2, cycling_radius = 1:n, pen_radius = 1:n + 0, transparency = 0.85, rotation = pi / 6, colors = cc, file = paste0("asdf", 1:n, ".svg")) %>% purrr::pmap(spiro) %>% image_merge( output = "emu_woman_sunset.svg") %>% image_scale(scale = seq(1, 0.1, length.out = n) ^ 0.85)
k <- 8 crossing(cycling_radius = 1:k, fixed_radius = k * 2 + 1) %>% rowid_to_column("id") %>% mutate( colors = lacroix_palette("Coconut", n = k , "continuous"), file = paste0("sdfds.", id, ".svg") ) %>% select(-id) %>% pmap( spiro, points_per_polygon = 2000, draw_fills = F, transparency = 0.9) %>% image_merge( output = "my_non_canonical_backstory.svg") %>% add_background()
rainbow_colors <- hsv( h = seq(1 / 16, 1, length.out = 16), s = 0.7, v = 0.7) spiro( fixed_radius = 16, cycling_radius = 5, pen_radius = 5, file = "licorice_donut_vivisection.svg", color_groups = 16, color_cycles = 2, points_per_polygon = 50, colors = rainbow_colors, transparency = 0.7) %>% add_background_gradient( colors = c("white", "black", "black", "white"), stops = c(.27, .34, .93, 1), rounding = 1, radius = 1)
n <- 10 oslo_colors <- scico( n = n, palette = "oslo", alpha = 0.9) %>% rev() spiro( file = "oslo_aster.svg", rotation = pi / 6, points_per_polygon = 100) %>% image_merge( output = "oslo_aster.svg", copies = n) %>% add_fills( colors = oslo_colors) %>% image_scale( scale = sqrt(0.75 ^ (seq(0, n - 1)))) %>% image_spin( rpm = 1:n + 1) %>% add_background( color = "black", rounding = 1) %>% add_restart()
set.seed(105) k <- 15 bg_colors <- paste0("gray", sample(1:k, k)) bg_stops <- sort(runif(k)) spiro( fixed_radius = 2 * 13 * 17, cycling_radius = 3 * 11 * 19, pen_radius = 171, file = "but_for_the_darkness_nothing_shimmers.svg", draw_fills = F, line_width = 3, color_groups = 380, color_cycles = 31, points_per_polygon = 100, colors = c( scico(60 * 2, palette = "lisbon", 0.8), scico(40 * 2, palette = "cork", 0.25), scico(20 * 2, palette = "lisbon", 1))) %>% add_background_gradient(rounding = 0, colors = bg_colors)
k <- 36 files <- paste0("s", 1:k, ".svg") pen_radii <- seq(3.8, 1.5, length.out = k) alphas <- rep_len(c(0.85, rep(0.2, 4)), k) colors <- rep_len(scico(6, palette = "devon"), k) %>% alpha(., alpha = alphas) tibble::tibble( file = files, pen_radius = pen_radii, colors = colors) %>% purrr::pmap_chr( spiro, fixed_radius = 7, cycling_radius = 4, rotation = -pi / 10, points_per_polygon = 500, draw_fills = T, xlim = c(-7, 7), ylim = c(-7, 7)) %>% image_merge( output = "youre_my_favorite.svg") %>% add_lines(colors = c(rep(NA,k - 1), "gray")) %>% image_rotate(degrees = (1:k / 2.5)) %>% add_background_gradient( colors = c( "#FFFFFF", "#26588E", "#E5E3F9", "#283568", "#C8C3F3"), radius = 1, rounding = 1, stops = c(0.42,0.93,0.96,0.97,1))
n <- 20 spiro( fixed_radius = 4, cycling_radius = 5, pen_radius = 1, file = "suspension_of_disbelief.svg") %>% image_merge( copies = n, output = "suspension_of_disbelief.svg") %>% add_fills( transparency = 1 / n, colors = "blue") %>% image_scale(scale = seq(1,0.1,length.out = n)) %>% image_spin(rpm = seq(0.5,10, length.out = n)) %>% add_restart()
set.seed(23) k <- 20 low <- 5 high <- 10 bg_colors <- paste0("gray", sample(low:high, k, replace = T)) bg_stops <- sort(runif(k, min = 0, max = .77)) spiro( fixed_radius = 359, cycling_radius = 261, pen_radius = 40, color_groups = 36, color_cycles = 36, draw_fills = F, points_per_polygon = 20, line_width = 3.5, file = "ride_ahead_to_make_the_fire.svg", colors = c( div_gradient_pal( low = "royalblue4", mid = "black", high = "firebrick4")(seq(0, 1, length.out = 18)), div_gradient_pal( low = "royalblue", mid = "white", high = "firebrick")(seq(0, 1, length.out = 18)))) %>% add_background_gradient(rounding = 0, colors = bg_colors, stops = bg_stops) %>% add_circle(color = "gray10", r = c(0.429, 0.48, 0.559,0.669,0.786, 0.886), line_width = 1.5)
my_purple <- scales::muted(scales::alpha("purple",alpha = 0.8)) tibble( colors = c(my_purple, "black"), fixed_radius = c(16, 15), cycling_radius = c(15, 14), file = c("purple.svg", "black.svg")) %>% pmap( spiro, pen_radius = 1.5, draw_fills = FALSE, line_width = 4) %>% image_merge(output = "violet_blackout.svg") %>% image_spin(rpm = c(0.5, -0.5)) %>% add_restart( color = my_purple, fill = "black") %>% add_background()
spiro( fixed_radius = 800, cycling_radius = 677, pen_radius = 100, color_groups = 10, color_cycles = 61, windings = 677 * 0.5, transparency = 1, start_angle = 0, points_per_polygon = 300, colors = scico(n = 10, palette = "cork"), draw_fills = F, file = "nanoscale_predictions.svg" ) %>% add_background_gradient( colors = c("black", "black", "gray40"))
c(spiro( fixed_radius = 21, cycling_radius = -20, pen_radius = 35, transparency = 0.2, colors = "black", file = "not_forgotten1.svg") %>% add_lines(colors = "#FFFFFFAA", line_width = .25), spiro( fixed_radius = 21, cycling_radius = -20, pen_radius = 35, transparency = .75, colors = "black", rotation = pi / 21, file = "not_forgotten2.svg") %>% add_lines(colors = "#FFFFFFAA", line_width = .5)) %>% image_merge(output = "not_forgotten.svg") %>% add_background_gradient( rounding = 1, radius = 1, colors = c("lightcyan2", rep(c("royalblue4", "lightcyan2"), 13),"royalblue4", rep("white",2)), stops = c(0, 0.05, 0.07, 0.11, 0.1578947, 0.2105263, 0.28, 0.3395, 0.39, 0.441, 0.485, 0.53, 0.571, 0.609, 0.6415, 0.682, 0.711, 0.745, 0.77, 0.80, 0.823, 0.84, 0.86, 0.88, 0.895, 0.905, 0.91, 0.92, 0.93, 1))
spiro( fixed_radius = 919, cycling_radius = 367, pen_radius = 509, windings = 403, color_groups = 17, color_cycles = 6, points_per_polygon = 500, transparency = 0.5, file = "when_time_reverses.svg", colors = scico(17, palette = "tofino") )
spiro( file = "purple_midnight.svg", fixed_radius = 800, cycling_radius = 751, pen_radius = 40, color_groups = 4, color_cycles = 2, points_per_polygon = 5000, colors = c( "midnightblue", "white", "purple4", "white")) %>% add_lines( colors = "black", line_width = 0.15) %>% add_background_gradient( stops = c(0,0.25,0.63,0.67,0.70,1), colors = c( "black", "purple4", "black", "midnightblue", "black", "gray20"))
set.seed(365) k <- 12 bg_colors <- scico(31, palette = "vik") %>% rev() %>% scales::muted(., l = 7, c = 7) %>% `[`(sample(1:31, k, replace = T)) bg_stops <- sort(runif(k)) spiro( fixed_radius = 1231, cycling_radius = 529, pen_radius = 1233, color_groups = 67, color_cycles = 59, windings = 101, points_per_polygon = 100, transparency = 1, colors = rev(scico(67, palette = "vik")), file = "skyscraper_sunrise.svg") %>% add_background_gradient(bg_colors, stops = bg_stops)
k <- 80 spiro(3,1,0.5, file = "asdf.svg", color_groups = 3, transparency = 0.5, colors = c("#AC1014", "#C0C0C0", "#175C02"), points_per_polygon = 100) %>% image_merge( copies = k, output = "counterspin_triangles.svg") %>% image_scale(scale = seq(1, 0.1, length.out = k)) %>% image_spin(rpm = rep(c(1, -1), k / 2) * seq(1, 3, length.out = k)) %>% add_background(rounding = 1) %>% add_restart()
k <- 80 spiro( 4, 3, 3, file = "asdf.svg", color_groups = 4, colors = rgb( c(0, .1418, .2118, .7012), c(.0039, .1608, .6392, 1), c(.3059, 0.9569, .9922, .9647), ) , transparency = 0.5, points_per_polygon = 100 ) %>% image_merge( copies = k, output = "illusively_elusive_allusion.svg") %>% image_scale(scale = seq(1, 0.2, length.out = k)) %>% image_spin(rpm = rep(c(1, -1), k / 2) * seq(1, 10, length.out = k)) %>% add_restart()
spiro( fixed_radius = 167, cycling_radius = 173, pen_radius = 14, windings = 173, color_groups = 52, color_cycles = 72 , points_per_polygon = 50, file = "blurry_eye.svg", colors = scico( n = 52, alpha = 0.9, palette = "roma")) %>% image_merge( copies = 6, output = "blurry_metal_eye.svg") %>% image_scale(seq(1, 0.95, length.out = 6)) %>% add_background(rounding = 1)
spiro( fixed_radius = 12 , cycling_radius = 121, pen_radius = 100, file = "maelstrom_sunset.svg", color_groups = 36, color_cycles = 48, points_per_polygon = 400, colors = scico(18,palette = "roma")) %>% add_background(rounding = 0)
k <- 40 tibble( fixed_radius = 5, cycling_radius = 3, pen_radius = seq(12, 3, length.out = k), colors = viridis::viridis(k, option = "D", alpha = 0.15), rotation = seq(5 * pi / k,0, length.out = k)) %>% pmap_chr(spiro) %>% image_merge( output = "wanna_rollerskate.svg") %>% image_rotate(degrees = -360 / 20) %>% # image_spin(1:k / k) %>% add_background()
ngroups <- 4 * 6 cc <- c( rainbow(ngroups / 4, s = 0.9, v = 0.3), rev(rainbow(ngroups / 4, s = 0.9, v = 0.3)), rainbow(ngroups / 4, s = 0.3, v = .9), rev(rainbow(ngroups / 4, s = 0.3, v = .9))) cc[seq(2, ngroups, 2)] <- "black" spiro( fixed_radius = 199, cycling_radius = 120, pen_radius = 43, color_groups = ngroups, color_cycles = 1440 %/% ngroups, points_per_polygon = 100, transparency = .3, draw_fills = T, colors = cc, file = "checkered_future.svg") %>% add_background_gradient( colors = c( "black", "black", "gray90", "black", "gray95", "black", "gray50"), stops = c( .00, .18, .20, .3, .50, .60, 1))
spiro( fixed_radius = 1, cycling_radius = sqrt(7), pen_radius = sqrt(7), windings = 450, draw_fills = F, colors = c( scico(11, palette = "tofino", direction = 1), scico(4, palette = "nuuk")), color_groups = 16, points_per_polygon = 100, color_cycles = 450 * 8, line_width = 0.75, lend = 1, file = "expensive_plaid.svg") %>% add_background()
cc <- hcl( h = seq(0,1,length.out = 52) * 220 + 70, c = 50, l = 28) cc[1:14] <- hcl( h = seq(0,1,length.out = 14) * 250 + 80, c = 60, l = 42) # cc <- scales::muted(cc, l = 40) spiro( fixed_radius = 44, cycling_radius = 13, pen_radius = 13, file = "asdf.svg", colors = cc, color_cycles = 4, draw_fills = F, color_groups = 4 * 13, points_per_polygon = 15, line_width = 0.3) %>% spiro::image_merge(., output = "groovy_church.svg", copies = 38) %>% image_rotate(1:36 * 0.21) %>% add_background_gradient( colors = c("gray15", "black", "black", "gray10", "black", "black", "gray15"), stops = c(0,0.27, 0.271, 0.5, .655, 0.656,1))
spiro( fixed_radius = 200, cycling_radius = 89, pen_radius = 175, color_groups = 200, color_cycles = 5, points_per_polygon = 1000, file = "hawaiian_snow_salad.svg", colors = alpha(lacroix_palette( name = "Coconut", n = 200, type = "continuous"), alpha = 0.8))
k <- 12 crossing( fixed_radius = k, cycling_radius = seq(1,k), pen_radius = k) %>% rowid_to_column("file") %>% mutate( file = paste0(file,".svg"), colors = c( scico( n = nrow(.) / 2, palette = "devon", alpha = .9), scico( n = nrow(.) / 2, palette = "devon", alpha = .7))) %>% pmap( spiro, draw_fills = T) %>% rev() -> p p[-1:-3] %>% rev %>% map2( seq(1,0.5, length.out = length(.)) ^ 0.4 , image_scale) %>% image_merge( output = "yes_you_may.svg") %>% add_lines(line_width = 0.2)
n <- 36 tibble( fixed_radius = 13, cycling_radius = 11, pen_radius = seq(1, n) / 2, file = paste0("sp", 1:n, ".svg"), colors = rep(scico( n = n / 4, alpha = 0.6, palette = "tofino"), times = 4)) %>% pmap(spiro, xlim = c(-20, 20), ylim = c(-20, 20)) %>% rev() %>% image_merge( output = "bending_is_not_a_compromise.svg") %>% image_rotate(sqrt(1:n * 50)) %>% add_background()
spiro( fixed_radius = 121, cycling_radius = 131, pen_radius = 11, file = "value_added_fragility.svg", color_groups = 121, color_cycles = 11, points_per_polygon = 100, colors = scico(11,palette = "cork")) %>% add_background(rounding = 0)
c( spiro( file = "restoration_agreement1.svg", fixed_radius = 800, cycling_radius = 677, pen_radius = 101, color_groups = 8, color_cycles = 677 * 3 , windings = 677 * 0.5, transparency = 1, rotation = pi / 13, points_per_polygon = 3, colors = paste0("gray", c(92,93,94,95,12,13,14,15)) ), spiro( file = "restoration_agreement2.svg", fixed_radius = 800, cycling_radius = 677, pen_radius = 101, color_groups = 8, color_cycles = 677 * 3 , windings = 677 * 0.5, transparency = 1, points_per_polygon = 3, colors = scico(n = 8, palette = "cork") )) %>% image_merge(output = "restoration_agreement.svg") %>% add_background(color = "gray8")
lim <- 5 k <- 36 tibble( pen_radius = seq(3, 1, length.out = k), colors = scico(n = k, palette = "cork") %>% scales::muted(., l = 50, c = 40), transparency = rep_len(c(rep(0.4, 3), 1), k), line_width = rep_len(c(rep(0.5, 3), 1), k) ) %>% pmap( spiro, fixed_radius = 5, cycling_radius = 3, rotation = -pi / 2, xlim = c(-lim, lim), ylim = c(-lim, lim), draw_fill = F ) %>% image_merge("convoluted_candor.svg", delete_input = T)
spiro( fixed_radius = 8, cycling_radius = 7, pen_radius = 1.069, color_groups = 8, color_cycles = 2, start_angle = 0, rotation = -pi / 8 , transparency = .5, points_per_polygon = 5000, windings = 7 * 6, file = "epistemological_modesty.svg", colors = dutchmasters_pal( palette = "milkmaid", reverse = T)(8))
spiro( fixed_radius = sqrt(13), cycling_radius = sqrt(11), pen_radius = sqrt(0.5), windings = 401, color_groups = 5, points_per_polygon = 40, color_cycles = 906, colors = c("darkred","gray90","darkgreen","gray20","forestgreen"), transparency = 0.55, file = "christmas_wreath.svg") %>% add_background_gradient( rounding = 1, radius = 1, colors = c("white",rep(c("gray30","black"), 5),"white"), stops = c(0.4,seq(.45,.9,length.out = 10),0.93))
n <- 10 spiro( fixed_radius = 6, cycling_radius = 11, pen_radius = -6 , colors = "white", file = "web_windows.svg", draw_fills = F, transparency = 0.9, line_width = 0.75, rotation = pi / 6, points_per_polygon = 1000) %>% image_merge( output = "web_windows.svg", copies = n) %>% add_fills(viridis_pal( end = 0.97, alpha = 0.9)(n)) %>% image_scale(scale = 1.07 * sqrt(0.8 ^ (seq(0, n - 1)))) %>% image_spin(rpm = 1:n * 0.15) %>% add_restart( fill = "#00000000", color = "white", ) %>% add_background()
tibble(colors = c("#004D47", "#128277")) %>% pmap( spiro, fixed_radius = pi, cycling_radius = sqrt(8), pen_radius = 0.5 * sqrt(8) / pi, windings = 81, start_angle = 0, points_per_polygon = 40001, transparency = 1) %>% tibble( input = ., duration = c(7, 11)) %>% pmap(image_animate, attribute = "opacity", values = c(0.2, 0.8, 0.2)) %>% image_merge( output = "colliding_scopes.svg", delete_input = T) %>% image_spin(rpm = c(0.1,-0.1)) %>% add_restart()
n <- 60 spiro(3, 1, 1, color_groups = 6, colors = lacroix_palettes$Tangerine[1,, drop = T] %>% scales::alpha(0.7), file = "delta.svg") %>% image_merge(copies = n, output = "the_increasing_returns_of_having_travelled.svg") %>% image_scale(seq(0.0001,1, length.out = n) ^ (1/1.25)) %>% image_rotate(seq(0,360 * 5.65, length.out = n)) %>% spiro::add_lines(input = ., colors = "gray10") %>% add_background_gradient(colors = c("gray30", "gray5","gray30","gray5"))
my_palette <- scales::colour_ramp(c( rgb(.74, .19, .22, .9), rgb(.86, .76, .22, .2), rgb(.91, .92, .92, .9), rgb(.11, .22, .22, .95), rgb(.22, .22, .22, .1), rgb(.24, .22, .71, .9) )) spiro( fixed_radius = 226, pen_radius = 235, cycling_radius = 15, points_per_polygon = 10, file = "old_errors.svg", color_groups = 40, color_cycles = 61, colors = my_palette(seq(0, 1, length.out = 40)) ) %>% add_background_gradient(colors = c("gray20", "white", "gray90", "gray30","gray5"), stops = c(0, .1, 0.6, .65, 1))
spiro( fixed_radius = 800, cycling_radius = 749, pen_radius = 51, color_groups = 4, color_cycles = 2, transparency = 0.25, start_angle = 0, points_per_polygon = 10000, rotation = -pi / 8, windings = 749, file = "the_past_was_never_simple.svg", colors = c( "firebrick4", "purple4", "midnightblue", "black")) %>% add_background_gradient( rounding = 0, stops = c(0, .08, 0.083, .6, 0.7, 1), colors = c( "white", "gray95", "gray40", "white", "white", "white"))
string_bezier( file = "portal_to_the_80s.svg", x = c(-1, 1, 0,-1, 1,-1, 0, 1,-1, 1), y = c(0, 0, 1, 0, 0, 0,-1, 0, 0, 0), color = c("#006B6B", "#FFFFFF", "black", "black", "black"), n = 200, lwd = 5, ljoin = 1) %>% add_background(color = "#108998")
Non-spirograph string art with Bézier curves
library(ggforce) k <- 30 r <- rep(0, k) r[1:2] <- 1 for (i in 3:k) { r[i] <- r[i - 1] * 1 / cos(pi / i) } xy <- function(n, r, dtheta) { tibble(x = r * cos((0:n) * dtheta + pi / 2), y = r * sin((0:n) * dtheta + pi / 2)) } svg("superhero.svg", width = 8, height = 8) par(pty = "s") k <- 30 r <- rep(0, k) r[1:2] <- 1 for (i in 3:k) { r[i] <- r[i - 1] * 1 / cos(pi / i) } plot( x = c(1, 2), y = c(1, 2), xlim = c(-8.7, 8.7), ylim = c(-8.7, 8.7), type = "n", axes = F, xlab = NA, ylab = NA, ann = F ) bg <- "slateblue" plotrix::draw.circle( 0, 0, radius = 8.7, col = bg, border = NA, nv = 200 ) tibble(n = 2:k, r = r[-1]) %>% mutate(dtheta = 2 * pi / n, d_xy = pmap(list(n, r, dtheta), xy)) %>% select(-dtheta) %>% arrange(-n) %>% purrr::pwalk(., function(n, r, d_xy) { plotrix::draw.circle( 0, 0, radius = r, col = paste0("gray", n * 3), border = bg, lwd = 2 * (2 / n) ^ (1 / 5), nv = 200 ) if (n > 2) polygon( d_xy, col = paste0("gray", 2), border = bg, lwd = 2 * (2 / n) ^ (1 / 5) ) }) dev.off()
Inscribed polygons adapted from Mathologer’s T-Shirt.
spiro( fixed_radius = 5, cycling_radius = 1, pen_radius = 11 / 3, color_groups = 5, colors = rep("gray95", 5), start_angle = pi, draw_fills = FALSE, rotation = pi / 10, file = "one_minute_reunion.svg", points_per_polygon = 250) %>% add_pathdot( colors = "firebrick", duration = c(2,3,4,5,6,10,12,15,20,30,60)) %>% # add_restart(color = "#333333",fill = scales::alpha("gray95")) %>% add_background("gray95")
When Ryan was learning about Least Common Multiples, he asked me if I ever used them outside of a math class. Here ya go, buddy!
What is the least common multiple of 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, and 60?
k <- 9 n_dots <- 120 cc <- lacroix_palette( name = "Coconut", n = n_dots) %>% alpha(0.7) s <- spiro( fixed_radius = 9, cycling_radius = 4, pen_radius = 6, draw_fills = F, colors = alpha(rep("black", k), 0.01), color_groups = k, points_per_polygon = 50, file = "asdf.svg") for (i in seq(0, n_dots - 1)) { s <- add_pathdot( s, delay = i / (n_dots / 12), colors = cc[i + 1], radius = 2, duration = 32) } image_merge( s, copies = 5, output = "sunrises_are_sunsets_recycled.svg") %>% image_spin(rpm = 1:5 / 5) %>% add_restart(color = "white", fill = "black") %>% add_background()
tibble( cycling_radius = c(-5,5), color_cycles = c(64,32), start_angle = c(0 * pi / 64 * 3, 20 * pi / 8), line_width = c(4 * 0.68, 4), file = c("s3.svg","s4.svg")) %>% pmap( spiro, fixed_radius = 8, pen_radius = 5, color_groups = 64, points_per_polygon = 2, draw_fills = F, ljoin = 2, end_at_beginning = T, colors = c( viridis_pal()(32), rev(viridis_pal()(32)))) %>% tibble(input = ., scale = c(1,0.68)) %>% pmap(image_scale) %>% image_merge( output = "graceful_exit.svg", delete_input = T) %>% add_background_gradient(colors = c("gray5", "gray25"))
As a kid, I loved making spirographs. I still do. Making them feels more like discovery than creativity, like finding hidden wings in the Mathematical Museum of Art. I have not yet found the point where spirographs no longer surprise me.
The surprising variety of forms generated by spirographs are manifestations of just one equation, the circular path troichoid. The shape of the spirograph depends on the radius of a fixed circle, radius of a cycling circle, and the distance of the pen from the center of the the cycling circle.
\[\begin{align} x (\theta) &= (R - r)\cos\theta + d\cos\left({R - r \over r}\theta\right)\\ y (\theta) &= (R - r)\sin\theta - d\sin\left({R - r \over r}\theta\right) \end{align} \]
Where
R is the radius of the fixed circle
r is the radius of the cycling circle
d is the distance of the pen from the center of the cycling circle
θ is the number of radians the cycling circle travels around the fixed circle
x(θ) is the position of x after the cycling circle travels θ radians
y(θ) is the position of y after the cycling circle travels θ radians
cycling_radius <- 1
fixed_radius <- 3
pen_radius <- 2
d_circle <- tibble(
x0 = c(0, fixed_radius - cycling_radius),
y0 = c(0, 0),
radius = c(fixed_radius, cycling_radius),
r_y = c(0, 0),
r_x = c(-fixed_radius / 2, fixed_radius - 1.5 * cycling_radius),
r_lab = c("Fixed\nRadius", "Cycling\nRadius"),
color = c("black", "royalblue")
)
d_segment <- tibble(
x = c(0, fixed_radius - cycling_radius, fixed_radius - cycling_radius),
y = c(0, 0, 0),
xend = c(-fixed_radius + 0.04, fixed_radius - cycling_radius * 2 + 0.04, fixed_radius - cycling_radius + pen_radius - 0.04),
yend = c(0, 0, 0),
color = c("black", "royalblue", "firebrick")
)
ggplot(data = d_circle) +
theme_void() +
ggforce::geom_circle(
aes(
x0 = x0,
y0 = y0,
r = radius,
color = color),
n = 1000) +
coord_equal() +
geom_text(
aes(
x = r_x,
y = r_y,
label = r_lab,
color = color),
vjust = 0.5,
nudge_y = 0.015,
angle = 0) +
annotate(
x = fixed_radius - cycling_radius + pen_radius / 2,
y = 0.015,
geom = "label",
color = "firebrick",
label = "Pen\nDistance",
label.size = 0,
label.padding = unit(3, "pt")) +
geom_segment(
data = d_segment,
aes(x = x, y = y, xend = xend, yend = yend, color = color),
geom = "segment",
linejoin = "mitre",
arrow = arrow(
length = unit(0.025, "npc"),
type = "closed",
angle = 15)) +
annotate(
x = fixed_radius - cycling_radius,
y = 0,
geom = "point",
color = "royalblue") +
scale_color_identity() +
theme(legend.position = "none")Three parameters of spirograph shapes
In this spirograph,
the fixed radius R is 3,
the cycling radius r is 1,
and the pen radius d is 2.
Although I still like making spirographs by hand, I wanted to extend what could be done with the traditional spirograph. I wrote the spiro package in R to make images that would be impossible to create on paper.
I cannot usually predict what will happen when I play with the three primary numbers of the equation. However, once a certain combination strikes me as interesting, I play with cutting it into different color segments to see if something interesting happens. Sometimes I merge many spirographs and spin them to see if the emerging patterns are pleasing.
Here I demonstrate what can be done with spiro package. I would love to see what you can do with it.
spiro(
fixed_radius = 1231,
cycling_radius = 529,
pen_radius = 1233,
colors = viridis::viridis(67),
color_groups = 67,
color_cycles = 59,
windings = 96,
points_per_polygon = 50,
file = "viridis_weave.svg"
) %>%
add_background(color = "gray8")tibble::tibble(
points_per_polygon = 1000,
fixed_radius = 17,
cycling_radius = 3:8,
colors = c(
"dodgerblue4", "white",
"dodgerblue3", "white",
"dodgerblue2", "white")) %>%
pmap_chr(spiro) %>%
image_merge(
output = "i_saw_your_movie.svg")n <- 25
cc <- ochre_palettes$emu_woman_paired[c(6, 11, 2, 7, 9)] %>%
rep(5)
tibble::tibble(
fixed_radius = n + 2,
cycling_radius = 1:n,
pen_radius = 1:n + 0,
transparency = 0.85,
rotation = pi / 6,
colors = cc,
file = paste0("asdf", 1:n, ".svg")) %>%
purrr::pmap(spiro) %>%
image_merge(
output = "emu_woman_sunset.svg") %>%
image_scale(scale = seq(1, 0.1, length.out = n) ^ 0.85)
k <- 8
crossing(cycling_radius = 1:k, fixed_radius = k * 2 + 1) %>%
rowid_to_column("id") %>%
mutate(
colors = lacroix_palette("Coconut", n = k , "continuous"),
file = paste0("sdfds.", id, ".svg")
) %>%
select(-id) %>%
pmap(
spiro,
points_per_polygon = 2000,
draw_fills = F,
transparency = 0.9) %>%
image_merge(
output = "my_non_canonical_backstory.svg") %>%
add_background()rainbow_colors <- hsv(
h = seq(1 / 16, 1, length.out = 16),
s = 0.7,
v = 0.7)
spiro(
fixed_radius = 16,
cycling_radius = 5,
pen_radius = 5,
file = "licorice_donut_vivisection.svg",
color_groups = 16,
color_cycles = 2,
points_per_polygon = 50,
colors = rainbow_colors,
transparency = 0.7) %>%
add_background_gradient(
colors = c("white", "black", "black", "white"),
stops = c(.27, .34, .93, 1),
rounding = 1,
radius = 1)n <- 10
oslo_colors <- scico(
n = n,
palette = "oslo",
alpha = 0.9) %>%
rev()
spiro(
file = "oslo_aster.svg",
rotation = pi / 6,
points_per_polygon = 100) %>%
image_merge(
output = "oslo_aster.svg",
copies = n) %>%
add_fills(
colors = oslo_colors) %>%
image_scale(
scale = sqrt(0.75 ^ (seq(0, n - 1)))) %>%
image_spin(
rpm = 1:n + 1) %>%
add_background(
color = "black",
rounding = 1) %>%
add_restart()set.seed(105)
k <- 15
bg_colors <- paste0("gray", sample(1:k, k))
bg_stops <- sort(runif(k))
spiro(
fixed_radius = 2 * 13 * 17,
cycling_radius = 3 * 11 * 19,
pen_radius = 171,
file = "but_for_the_darkness_nothing_shimmers.svg",
draw_fills = F,
line_width = 3,
color_groups = 380,
color_cycles = 31,
points_per_polygon = 100,
colors = c(
scico(60 * 2, palette = "lisbon", 0.8),
scico(40 * 2, palette = "cork", 0.25),
scico(20 * 2, palette = "lisbon", 1))) %>%
add_background_gradient(rounding = 0, colors = bg_colors)k <- 36
files <- paste0("s", 1:k, ".svg")
pen_radii <- seq(3.8, 1.5, length.out = k)
alphas <- rep_len(c(0.85, rep(0.2, 4)), k)
colors <- rep_len(scico(6, palette = "devon"), k) %>%
alpha(., alpha = alphas)
tibble::tibble(
file = files,
pen_radius = pen_radii,
colors = colors) %>%
purrr::pmap_chr(
spiro,
fixed_radius = 7,
cycling_radius = 4,
rotation = -pi / 10,
points_per_polygon = 500,
draw_fills = T,
xlim = c(-7, 7),
ylim = c(-7, 7)) %>%
image_merge(
output = "youre_my_favorite.svg") %>%
add_lines(colors = c(rep(NA,k - 1), "gray")) %>%
image_rotate(degrees = (1:k / 2.5)) %>%
add_background_gradient(
colors = c(
"#FFFFFF",
"#26588E",
"#E5E3F9",
"#283568",
"#C8C3F3"),
radius = 1,
rounding = 1,
stops = c(0.42,0.93,0.96,0.97,1))n <- 20
spiro(
fixed_radius = 4,
cycling_radius = 5,
pen_radius = 1,
file = "suspension_of_disbelief.svg") %>%
image_merge(
copies = n,
output = "suspension_of_disbelief.svg") %>%
add_fills(
transparency = 1 / n,
colors = "blue") %>%
image_scale(scale = seq(1,0.1,length.out = n)) %>%
image_spin(rpm = seq(0.5,10, length.out = n)) %>%
add_restart()set.seed(23)
k <- 20
low <- 5
high <- 10
bg_colors <- paste0("gray", sample(low:high, k, replace = T))
bg_stops <- sort(runif(k, min = 0, max = .77))
spiro(
fixed_radius = 359,
cycling_radius = 261,
pen_radius = 40,
color_groups = 36,
color_cycles = 36,
draw_fills = F,
points_per_polygon = 20,
line_width = 3.5,
file = "ride_ahead_to_make_the_fire.svg",
colors = c(
div_gradient_pal(
low = "royalblue4",
mid = "black",
high = "firebrick4")(seq(0, 1, length.out = 18)),
div_gradient_pal(
low = "royalblue",
mid = "white",
high = "firebrick")(seq(0, 1, length.out = 18)))) %>%
add_background_gradient(rounding = 0, colors = bg_colors, stops = bg_stops) %>%
add_circle(color = "gray10", r = c(0.429, 0.48, 0.559,0.669,0.786, 0.886), line_width = 1.5) my_purple <- scales::muted(scales::alpha("purple",alpha = 0.8))
tibble(
colors = c(my_purple, "black"),
fixed_radius = c(16, 15),
cycling_radius = c(15, 14),
file = c("purple.svg", "black.svg")) %>%
pmap(
spiro,
pen_radius = 1.5,
draw_fills = FALSE,
line_width = 4) %>%
image_merge(output = "violet_blackout.svg") %>%
image_spin(rpm = c(0.5, -0.5)) %>%
add_restart(
color = my_purple,
fill = "black") %>%
add_background() spiro(
fixed_radius = 800,
cycling_radius = 677,
pen_radius = 100,
color_groups = 10,
color_cycles = 61,
windings = 677 * 0.5,
transparency = 1,
start_angle = 0,
points_per_polygon = 300,
colors = scico(n = 10, palette = "cork"),
draw_fills = F,
file = "nanoscale_predictions.svg"
) %>%
add_background_gradient(
colors = c("black", "black", "gray40"))c(spiro(
fixed_radius = 21,
cycling_radius = -20,
pen_radius = 35,
transparency = 0.2,
colors = "black",
file = "not_forgotten1.svg") %>%
add_lines(colors = "#FFFFFFAA", line_width = .25),
spiro(
fixed_radius = 21,
cycling_radius = -20,
pen_radius = 35,
transparency = .75,
colors = "black",
rotation = pi / 21,
file = "not_forgotten2.svg") %>%
add_lines(colors = "#FFFFFFAA", line_width = .5)) %>%
image_merge(output = "not_forgotten.svg") %>%
add_background_gradient(
rounding = 1,
radius = 1,
colors = c("lightcyan2",
rep(c("royalblue4", "lightcyan2"), 13),"royalblue4",
rep("white",2)),
stops = c(0,
0.05, 0.07,
0.11, 0.1578947,
0.2105263, 0.28,
0.3395, 0.39,
0.441, 0.485,
0.53, 0.571,
0.609, 0.6415,
0.682, 0.711,
0.745, 0.77,
0.80, 0.823,
0.84, 0.86,
0.88, 0.895,
0.905, 0.91,
0.92, 0.93, 1))spiro(
fixed_radius = 919,
cycling_radius = 367,
pen_radius = 509,
windings = 403,
color_groups = 17,
color_cycles = 6,
points_per_polygon = 500,
transparency = 0.5,
file = "when_time_reverses.svg",
colors = scico(17, palette = "tofino")
)spiro(
file = "purple_midnight.svg",
fixed_radius = 800,
cycling_radius = 751,
pen_radius = 40,
color_groups = 4,
color_cycles = 2,
points_per_polygon = 5000,
colors = c(
"midnightblue",
"white",
"purple4",
"white")) %>%
add_lines(
colors = "black",
line_width = 0.15) %>%
add_background_gradient(
stops = c(0,0.25,0.63,0.67,0.70,1),
colors = c(
"black",
"purple4",
"black",
"midnightblue",
"black",
"gray20")) set.seed(365)
k <- 12
bg_colors <- scico(31, palette = "vik") %>%
rev() %>%
scales::muted(., l = 7, c = 7) %>%
`[`(sample(1:31, k, replace = T))
bg_stops <- sort(runif(k))
spiro(
fixed_radius = 1231,
cycling_radius = 529,
pen_radius = 1233,
color_groups = 67,
color_cycles = 59,
windings = 101,
points_per_polygon = 100,
transparency = 1,
colors = rev(scico(67, palette = "vik")),
file = "skyscraper_sunrise.svg") %>%
add_background_gradient(bg_colors, stops = bg_stops)k <- 80
spiro(3,1,0.5,
file = "asdf.svg",
color_groups = 3,
transparency = 0.5,
colors = c("#AC1014", "#C0C0C0", "#175C02"),
points_per_polygon = 100) %>%
image_merge(
copies = k,
output = "counterspin_triangles.svg") %>%
image_scale(scale = seq(1, 0.1, length.out = k)) %>%
image_spin(rpm = rep(c(1, -1), k / 2) * seq(1, 3, length.out = k)) %>%
add_background(rounding = 1) %>%
add_restart()k <- 80
spiro(
4,
3,
3,
file = "asdf.svg",
color_groups = 4,
colors = rgb(
c(0, .1418, .2118, .7012),
c(.0039, .1608, .6392, 1),
c(.3059, 0.9569, .9922, .9647),
) ,
transparency = 0.5,
points_per_polygon = 100
) %>%
image_merge(
copies = k,
output = "illusively_elusive_allusion.svg") %>%
image_scale(scale = seq(1, 0.2, length.out = k)) %>%
image_spin(rpm = rep(c(1, -1), k / 2) * seq(1, 10, length.out = k)) %>%
add_restart()spiro(
fixed_radius = 167,
cycling_radius = 173,
pen_radius = 14,
windings = 173,
color_groups = 52,
color_cycles = 72 ,
points_per_polygon = 50,
file = "blurry_eye.svg",
colors = scico(
n = 52,
alpha = 0.9,
palette = "roma")) %>%
image_merge(
copies = 6,
output = "blurry_metal_eye.svg") %>%
image_scale(seq(1, 0.95, length.out = 6)) %>%
add_background(rounding = 1)spiro(
fixed_radius = 12 ,
cycling_radius = 121,
pen_radius = 100,
file = "maelstrom_sunset.svg",
color_groups = 36,
color_cycles = 48,
points_per_polygon = 400,
colors = scico(18,palette = "roma")) %>%
add_background(rounding = 0)k <- 40
tibble(
fixed_radius = 5,
cycling_radius = 3,
pen_radius = seq(12, 3, length.out = k),
colors = viridis::viridis(k, option = "D", alpha = 0.15),
rotation = seq(5 * pi / k,0, length.out = k)) %>%
pmap_chr(spiro) %>%
image_merge(
output = "wanna_rollerskate.svg") %>%
image_rotate(degrees = -360 / 20) %>%
# image_spin(1:k / k) %>%
add_background()ngroups <- 4 * 6
cc <- c(
rainbow(ngroups / 4, s = 0.9, v = 0.3),
rev(rainbow(ngroups / 4, s = 0.9, v = 0.3)),
rainbow(ngroups / 4, s = 0.3, v = .9),
rev(rainbow(ngroups / 4, s = 0.3, v = .9)))
cc[seq(2, ngroups, 2)] <- "black"
spiro(
fixed_radius = 199,
cycling_radius = 120,
pen_radius = 43,
color_groups = ngroups,
color_cycles = 1440 %/% ngroups,
points_per_polygon = 100,
transparency = .3,
draw_fills = T,
colors = cc,
file = "checkered_future.svg") %>%
add_background_gradient(
colors = c(
"black", "black",
"gray90", "black",
"gray95", "black",
"gray50"),
stops = c(
.00, .18,
.20, .3,
.50, .60,
1))spiro(
fixed_radius = 1,
cycling_radius = sqrt(7),
pen_radius = sqrt(7),
windings = 450,
draw_fills = F,
colors = c(
scico(11, palette = "tofino", direction = 1),
scico(4, palette = "nuuk")),
color_groups = 16,
points_per_polygon = 100,
color_cycles = 450 * 8,
line_width = 0.75,
lend = 1,
file = "expensive_plaid.svg") %>%
add_background()cc <- hcl(
h = seq(0,1,length.out = 52) * 220 + 70,
c = 50,
l = 28)
cc[1:14] <- hcl(
h = seq(0,1,length.out = 14) * 250 + 80,
c = 60,
l = 42)
# cc <- scales::muted(cc, l = 40)
spiro(
fixed_radius = 44,
cycling_radius = 13,
pen_radius = 13,
file = "asdf.svg",
colors = cc,
color_cycles = 4,
draw_fills = F,
color_groups = 4 * 13,
points_per_polygon = 15,
line_width = 0.3) %>%
spiro::image_merge(.,
output = "groovy_church.svg",
copies = 38) %>%
image_rotate(1:36 * 0.21) %>%
add_background_gradient(
colors = c("gray15", "black",
"black", "gray10", "black",
"black", "gray15"),
stops = c(0,0.27,
0.271, 0.5, .655,
0.656,1))spiro(
fixed_radius = 200,
cycling_radius = 89,
pen_radius = 175,
color_groups = 200,
color_cycles = 5,
points_per_polygon = 1000,
file = "hawaiian_snow_salad.svg",
colors = alpha(lacroix_palette(
name = "Coconut",
n = 200,
type = "continuous"),
alpha = 0.8))k <- 12
crossing(
fixed_radius = k,
cycling_radius = seq(1,k),
pen_radius = k) %>%
rowid_to_column("file") %>%
mutate(
file = paste0(file,".svg"),
colors = c(
scico(
n = nrow(.) / 2,
palette = "devon",
alpha = .9),
scico(
n = nrow(.) / 2,
palette = "devon",
alpha = .7))) %>%
pmap(
spiro,
draw_fills = T) %>%
rev() -> p
p[-1:-3] %>%
rev %>%
map2(
seq(1,0.5, length.out = length(.)) ^ 0.4 ,
image_scale) %>%
image_merge(
output = "yes_you_may.svg") %>%
add_lines(line_width = 0.2) n <- 36
tibble(
fixed_radius = 13,
cycling_radius = 11,
pen_radius = seq(1, n) / 2,
file = paste0("sp", 1:n, ".svg"),
colors = rep(scico(
n = n / 4,
alpha = 0.6,
palette = "tofino"),
times = 4)) %>%
pmap(spiro,
xlim = c(-20, 20),
ylim = c(-20, 20)) %>%
rev() %>%
image_merge(
output = "bending_is_not_a_compromise.svg") %>%
image_rotate(sqrt(1:n * 50)) %>%
add_background()spiro(
fixed_radius = 121,
cycling_radius = 131,
pen_radius = 11,
file = "value_added_fragility.svg",
color_groups = 121,
color_cycles = 11,
points_per_polygon = 100,
colors = scico(11,palette = "cork")) %>%
add_background(rounding = 0)c(
spiro(
file = "restoration_agreement1.svg",
fixed_radius = 800,
cycling_radius = 677,
pen_radius = 101,
color_groups = 8,
color_cycles = 677 * 3 ,
windings = 677 * 0.5,
transparency = 1,
rotation = pi / 13,
points_per_polygon = 3,
colors = paste0("gray", c(92,93,94,95,12,13,14,15))
),
spiro(
file = "restoration_agreement2.svg",
fixed_radius = 800,
cycling_radius = 677,
pen_radius = 101,
color_groups = 8,
color_cycles = 677 * 3 ,
windings = 677 * 0.5,
transparency = 1,
points_per_polygon = 3,
colors = scico(n = 8, palette = "cork")
)) %>%
image_merge(output = "restoration_agreement.svg") %>%
add_background(color = "gray8")lim <- 5
k <- 36
tibble(
pen_radius = seq(3, 1, length.out = k),
colors = scico(n = k, palette = "cork") %>%
scales::muted(., l = 50, c = 40),
transparency = rep_len(c(rep(0.4, 3), 1), k),
line_width = rep_len(c(rep(0.5, 3), 1), k)
) %>%
pmap(
spiro,
fixed_radius = 5,
cycling_radius = 3,
rotation = -pi / 2,
xlim = c(-lim, lim),
ylim = c(-lim, lim),
draw_fill = F
) %>%
image_merge("convoluted_candor.svg", delete_input = T)spiro(
fixed_radius = 8,
cycling_radius = 7,
pen_radius = 1.069,
color_groups = 8,
color_cycles = 2,
start_angle = 0,
rotation = -pi / 8 ,
transparency = .5,
points_per_polygon = 5000,
windings = 7 * 6,
file = "epistemological_modesty.svg",
colors = dutchmasters_pal(
palette = "milkmaid",
reverse = T)(8))
spiro(
fixed_radius = sqrt(13),
cycling_radius = sqrt(11),
pen_radius = sqrt(0.5),
windings = 401,
color_groups = 5,
points_per_polygon = 40,
color_cycles = 906,
colors = c("darkred","gray90","darkgreen","gray20","forestgreen"),
transparency = 0.55,
file = "christmas_wreath.svg") %>%
add_background_gradient(
rounding = 1,
radius = 1,
colors = c("white",rep(c("gray30","black"), 5),"white"),
stops = c(0.4,seq(.45,.9,length.out = 10),0.93))n <- 10
spiro(
fixed_radius = 6,
cycling_radius = 11,
pen_radius = -6 ,
colors = "white",
file = "web_windows.svg",
draw_fills = F,
transparency = 0.9,
line_width = 0.75,
rotation = pi / 6,
points_per_polygon = 1000) %>%
image_merge(
output = "web_windows.svg",
copies = n) %>%
add_fills(viridis_pal(
end = 0.97,
alpha = 0.9)(n)) %>%
image_scale(scale = 1.07 * sqrt(0.8 ^ (seq(0, n - 1)))) %>%
image_spin(rpm = 1:n * 0.15) %>%
add_restart(
fill = "#00000000",
color = "white",
) %>%
add_background()tibble(colors = c("#004D47", "#128277")) %>%
pmap(
spiro,
fixed_radius = pi,
cycling_radius = sqrt(8),
pen_radius = 0.5 * sqrt(8) / pi,
windings = 81,
start_angle = 0,
points_per_polygon = 40001,
transparency = 1) %>%
tibble(
input = .,
duration = c(7, 11)) %>%
pmap(image_animate,
attribute = "opacity",
values = c(0.2, 0.8, 0.2)) %>%
image_merge(
output = "colliding_scopes.svg",
delete_input = T) %>%
image_spin(rpm = c(0.1,-0.1)) %>%
add_restart()n <- 60
spiro(3, 1, 1, color_groups = 6, colors = paletteer::palettes_d$LaCroixColoR$Tangerine %>% scales::alpha(0.7), file = "delta.svg") %>%
image_merge(copies = n, output = "the_increasing_returns_of_having_travelled.svg") %>%
image_scale(seq(0.0001,1, length.out = n) %>% `^`(1/1.25)) %>%
image_rotate(seq(0,360 * 5.65, length.out = n)) %>%
spiro::add_lines(input = ., colors = "gray10") %>%
add_background_gradient(colors = c("gray30", "gray5","gray30","gray5"))spiro(
fixed_radius = 800,
cycling_radius = 749,
pen_radius = 51,
color_groups = 4,
color_cycles = 2,
transparency = 0.25,
start_angle = 0,
points_per_polygon = 10000,
rotation = -pi / 8,
windings = 749,
file = "the_past_was_never_simple.svg",
colors = c(
"firebrick4",
"purple4",
"midnightblue",
"black")) %>%
add_background_gradient(
rounding = 0,
stops = c(0, .08, 0.083, .6, 0.7, 1),
colors = c(
"white",
"gray95",
"gray40",
"white",
"white",
"white"))string_bezier(
file = "portal_to_the_80s.svg",
x = c(-1, 1, 0,-1, 1,-1, 0, 1,-1, 1),
y = c(0, 0, 1, 0, 0, 0,-1, 0, 0, 0),
color = c("#006B6B", "#FFFFFF", "black", "black", "black"),
n = 200,
lwd = 5,
ljoin = 1) %>%
add_background(color = "#108998")Non-spirograph string art with Bézier curves
library(ggforce)
k <- 30
r <- rep(0, k)
r[1:2] <- 1
for (i in 3:k) {
r[i] <- r[i - 1] * 1 / cos(pi / i)
}
xy <- function(n, r, dtheta) {
tibble(x = r * cos((0:n) * dtheta + pi / 2),
y = r * sin((0:n) * dtheta + pi / 2))
}
svg("superhero.svg", width = 8, height = 8)
par(pty = "s")
k <- 30
r <- rep(0, k)
r[1:2] <- 1
for (i in 3:k) {
r[i] <- r[i - 1] * 1 / cos(pi / i)
}
plot(
x = c(1, 2),
y = c(1, 2),
xlim = c(-8.7, 8.7),
ylim = c(-8.7, 8.7),
type = "n",
axes = F,
xlab = NA,
ylab = NA,
ann = F
)
bg <- "slateblue"
plotrix::draw.circle(
0,
0,
radius = 8.7,
col = bg,
border = NA,
nv = 200
)
tibble(n = 2:k, r = r[-1]) %>%
mutate(dtheta = 2 * pi / n,
d_xy = pmap(list(n, r, dtheta), xy)) %>%
select(-dtheta) %>%
arrange(-n) %>%
purrr::pwalk(., function(n, r, d_xy) {
plotrix::draw.circle(
0,
0,
radius = r,
col = paste0("gray", n * 3),
border = bg,
lwd = 2 * (2 / n) ^ (1 / 5),
nv = 200
)
if (n > 2)
polygon(
d_xy,
col = paste0("gray", 2),
border = bg,
lwd
= 2 * (2 / n) ^ (1 / 5)
)
})
dev.off()Inscribed polygons adapted from Mathologer’s T-Shirt.
spiro(
fixed_radius = 5,
cycling_radius = 1,
pen_radius = 11 / 3,
color_groups = 5,
colors = rep("gray95", 5),
start_angle = pi,
draw_fills = FALSE,
rotation = pi / 10,
file = "one_minute_reunion.svg",
points_per_polygon = 250) %>%
add_pathdot(
colors = "firebrick",
duration = c(2,3,4,5,6,10,12,15,20,30,60)) %>%
# add_restart(color = "#333333",fill = scales::alpha("gray95")) %>%
add_background("gray95")When Ryan was learning about Least Common Multiples, he asked me if I ever used them outside of a math class. Here ya go, buddy!
What is the least common multiple of 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, and 60?
k <- 9
n_dots <- 120
cc <- lacroix_palette(
name = "Coconut",
n = n_dots) %>%
alpha(0.7)
s <- spiro(
fixed_radius = 9,
cycling_radius = 4,
pen_radius = 6,
draw_fills = F,
colors = alpha(rep("black", k), 0.01),
color_groups = k,
points_per_polygon = 50,
file = "asdf.svg")
for (i in seq(0, n_dots - 1)) {
s <- add_pathdot(
s,
delay = i / (n_dots / 12),
colors = cc[i + 1],
radius = 2,
duration = 32)
}
image_merge(
s,
copies = 5,
output = "sunrises_are_sunsets_recycled.svg") %>%
image_spin(rpm = 1:5 / 5) %>%
add_restart(color = "white", fill = "black") %>%
add_background()tibble(
cycling_radius = c(-5,5),
color_cycles = c(64,32),
start_angle = c(0 * pi / 64 * 3, 20 * pi / 8),
line_width = c(4 * 0.68, 4),
file = c("s3.svg","s4.svg")) %>%
pmap(
spiro,
fixed_radius = 8,
pen_radius = 5,
color_groups = 64,
points_per_polygon = 2,
draw_fills = F,
ljoin = 2,
end_at_beginning = T,
colors = c(
viridis_pal()(32),
rev(viridis_pal()(32)))) %>%
tibble(input = ., scale = c(1,0.68)) %>%
pmap(image_scale) %>%
image_merge(
output = "graceful_exit.svg",
delete_input = T) %>%
add_background_gradient(colors = c("gray5", "gray25"))